<!DOCTYPE html>
<html lang="en">
	<head>
		<title>Three.js MMDLoader app GPU Water</title>
		<meta charset="utf-8">
		<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
		<style>
			body {
				font-family: Monospace;
				background-color: #fff;
				color: #000;
				margin: 0px;
				overflow: hidden;
			}
			#info {
				color: #000;
				position: absolute;
				top: 10px;
				width: 100%;
				text-align: center;
				display:block;
			}
			#info a, .button { color: #f00; font-weight: bold; text-decoration: underline; cursor: pointer }
		</style>
	</head>

	<body>
		<script id="vertexShaderWater" type="x-shader/x-vertex">
			varying vec2 vUv;
			varying vec3 vNormal;
			varying vec3 vPosition;
			varying mat4 vProjectionMatrix;
			varying mat4 vModelViewMatrix;

			uniform float time;

			const int maxCircleWaveNum = <maxCircleWaveNum>;

			uniform int circleWaveNum;
			uniform float circleWaveTriggerTimes[ maxCircleWaveNum ];
			uniform vec2 circleWaveCenters[ maxCircleWaveNum ];

			const int maxDirectionWaveNum = <maxDirectionWaveNum>;

			uniform int directionWaveNum;
			uniform vec2 directionWaveVectors[ maxDirectionWaveNum ];

			uniform sampler2D tWaterNormalMap;

			const float circleWaveLength = 0.05;
			const float circleWaveAmplitude = 0.05;
			const float circleWaveK = 2.0;
			const float circleWaveSpeed = 0.4;

			const float directionWaveLength = 0.1;
			const float directionWaveAmplitude = 0.1;
			const float directionWaveK = 1.0;
			const float directionWaveSpeed = 0.3;

			vec2 getPosition() {

				return uv * 2.0 - 1.0;

			}

			float getTheta( vec2 direction ) {

				return dot( direction, getPosition() );

			}

			float wave(
				vec2 direction,
				float waveLength,
				float amplitude,
				float k,
				float speed,
				float t
			) {

				float frequency = 2.0 / waveLength;
				float phi = speed * frequency;

				float theta = getTheta( direction );

				return 2.0 * amplitude * pow( ( sin( theta * frequency + t * phi ) + 1.0 ) / 2.0, k );

			}

			vec2 vwave(
				vec2 direction,
				float waveLength,
				float amplitude,
				float k,
				float speed,
				float t
			) {

				float frequency = 2.0 / waveLength;
				float phi = speed * frequency;

				float theta = getTheta( direction );

				return k * frequency * amplitude * pow( ( sin( theta * frequency + time * phi ) + 1.0 ) / 2.0, k - 1.0 ) * cos( theta * frequency + t * phi ) * direction;

			}

			float getPastTime( float triggerTime ) {

				return time - triggerTime;

			}

			vec2 getCircleDirection( vec2 center ) {

				return -normalize( getPosition() - center );

			}

			float getCircleDistance( vec2 center ) {

				return length( getPosition() - center );

			}

			bool checkIfCircleWave( vec2 center, float triggerTime ) {

				//return true;

				float distance = getCircleDistance( center );
				float pastTime = getPastTime( triggerTime );

				if ( ( distance > ( pastTime + circleWaveLength * 10.0 ) * circleWaveSpeed ) ||
				     ( distance < ( pastTime - circleWaveLength * 10.0 ) * circleWaveSpeed ) ) return false;

				return true;

			}

			float getAttenuation( float triggerTime ) {

				float pastTime = getPastTime( triggerTime );

				return max( pastTime * 4.0, 1.0 );

			}

			float circleWave( vec2 center, float triggerTime ) {

				if ( ! checkIfCircleWave( center, triggerTime ) ) return 0.0;

				return wave(
					getCircleDirection( center ),
					circleWaveLength,
					circleWaveAmplitude,
					circleWaveK,
					circleWaveSpeed,
					getPastTime( triggerTime )
				) / getAttenuation( triggerTime );

			}

			vec2 vCircleWave( vec2 center, float triggerTime ) {

				if ( ! checkIfCircleWave( center, triggerTime ) ) return vec2( 0.0 );

				return vwave(
					getCircleDirection( center ),
					circleWaveLength,
					circleWaveAmplitude,
					circleWaveK,
					circleWaveSpeed,
					getPastTime( triggerTime )
				) / getAttenuation( triggerTime );

			}

			float directionWave( vec2 direction ) {

				return wave(
					direction,
					directionWaveLength,
					directionWaveAmplitude,
					directionWaveK,
					directionWaveSpeed,
					time
				);

			}

			vec2 vDirectionWave( vec2 direction ) {

				return vwave(
					direction,
					directionWaveLength,
					directionWaveAmplitude,
					directionWaveK,
					directionWaveSpeed,
					time
				);

			}

			void main() {

				float offset = 0.0;
				vec2 vative = vec2( 0.0 );

				for ( int i = 0; i < maxCircleWaveNum; i ++ ) {

					if ( i >= circleWaveNum ) break;

					vec2 trigger = vec2( circleWaveCenters[ i ] );

					offset += circleWave( circleWaveCenters[ i ], circleWaveTriggerTimes[ i ] );
					vative += vCircleWave( circleWaveCenters[ i ], circleWaveTriggerTimes[ i ] );

				}

				for ( int i = 0; i < maxDirectionWaveNum; i ++ ) {

					if ( i >= directionWaveNum ) break;

					offset += directionWave( directionWaveVectors[ i ] );
					vative += vDirectionWave( directionWaveVectors[ i ] );

				}

				vec3 newPosition = vec3( position.xy, position.z + offset );

				gl_Position = projectionMatrix * modelViewMatrix * vec4( newPosition, 1.0 );

				vec3 newNormal = normalize(
				                   ( normal + vec3( -vative.x, -vative.y, 1.0 ) * 0.1 ) *
				                   texture2D( tWaterNormalMap, uv ).xyz
				                 );

				vUv = uv;
				vNormal = normalize( normalMatrix * newNormal );
				vPosition = newPosition;
				vProjectionMatrix = projectionMatrix;
				vModelViewMatrix = modelViewMatrix;

			}
		</script>

		<script id="fragmentShaderWater" type="x-shader/x-fragment">
			varying vec2 vUv;
			varying vec3 vNormal;
			varying vec3 vPosition;
			varying mat4 vProjectionMatrix;
			varying mat4 vModelViewMatrix;

			uniform vec2 size;
			uniform sampler2D tDiffuse;
			uniform sampler2D tWaterMap;

			void main() {

				vec3 lightPos = vec3( 50.0, 50.0, 50.0 );
				vec3 lightVec = normalize( -lightPos );
				vec3 eyeVec = normalize( -cameraPosition );

				vec3 waveNormal = normalize( vNormal );
				vec3 mirrorNormal = normalize( vec3( 0.0, 0.0, 1.0 ) );
				vec3 flatNormal = waveNormal - dot( waveNormal, mirrorNormal ) * mirrorNormal;
				vec3 eyeNormal = ( vProjectionMatrix * vModelViewMatrix * vec4( flatNormal, 1.0 ) ).xyz;

				vec2 coordOffset = normalize( eyeNormal.xy ) * length( flatNormal ) * 0.01;

				vec2 coord = gl_FragCoord.xy / size;
				vec4 textureColor = texture2D( tDiffuse, coord + coordOffset ) + texture2D( tWaterMap, vUv );

				vec3 ambientColor = vec3( 0.0, 0.4, 0.8 );
				vec3 diffuseColor = vec3( 0.0, 0.4, 0.8 );
				vec3 specularColor = vec3( 0.2, 0.2, 0.2 );

				vec3 surfaceToLight = normalize( lightPos - vPosition );
				vec3 surfaceToCamera = normalize( cameraPosition - vPosition );

				float shininess = 20.0;

				vec3 ambient = ambientColor;
				vec3 diffuse = diffuseColor * max( dot( vNormal, lightVec ), 0.0 );
				vec3 specular = specularColor * pow( max( 0.0, dot( surfaceToCamera, reflect( -surfaceToLight, vNormal ) ) ), shininess );

				vec4 color = textureColor * vec4( ambient + diffuse + specular, 0.3 );

				gl_FragColor = color;

			}
		</script>

		<script id="vertexShaderSplash" type="x-shader/x-vertex">
			attribute float inputTime;
			attribute vec3 translate;
			attribute vec3 vector;

			uniform float time;

			varying vec3 vOffset;

			const float g = 9.8 * 4.0;

			void main() {

				float t = time - inputTime;

				vec3 offset = vec3( 0.0 );
				offset.xz = vector.xz * ( t + 0.4 );
				offset.y = vector.y * t - 0.5 * g * t * t;

				gl_Position = projectionMatrix * modelViewMatrix * vec4( position + translate + offset, 1.0 );

				vOffset = offset;

			}
		</script>

		<script id="fragmentShaderSplash" type="x-shader/x-fragment">
			varying vec3 vOffset;

			void main() {

				if ( vOffset.y <= 0.0 ) discard;

				gl_FragColor = vec4( 0.8, 1.0, 1.0, 0.3 );

			}
		</script>

		<div id="info">
		<a href="http://threejs.org" target="_blank">three.js</a> - MMDLoader app GPU Water<br />
		<a href="https://github.com/takahirox/MMDLoader-app#readme" target="_blank">MMD Assets license</a><br />
		Copyright
		<a href="http://threejs.org" target="_blank">three.js</a>
		<a href="http://www.geocities.jp/higuchuu4/index_e.htm" target="_blank">Model Data</a>
		<a href="http://www.nicovideo.jp/watch/sm13147122" target="_blank">Dance Data</a>
		</div>

		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/build/three.js"></script>

		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/libs/mmdparser.min.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/libs/ammo.js"></script>

		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/loaders/TGALoader.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/loaders/MMDLoader.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/effects/OutlineEffect.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/animation/CCDIKSolver.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/animation/MMDPhysics.js"></script>

		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/controls/OrbitControls.js"></script>

		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/Detector.js"></script>
		<script src="https://rawcdn.githack.com/mrdoob/three.js/r87/examples/js/libs/dat.gui.min.js"></script>

		<script>

			var container;

			var scene;
			var camera;

			var renderer;
			var effect;

			var mesh;
			var water;
			var splash;
			var renderTarget;

			var helper;

			var waterSize = 64;

			var waterTextureFile = 'https://rawcdn.githack.com/mrdoob/three.js/r87/examples/textures/water.jpg';
			var waterNormalTextureFile = 'https://rawcdn.githack.com/mrdoob/three.js/r87/examples/textures/waternormals.jpg';

			// see the license https://github.com/takahirox/MMDLoader-app#readme for these assets
			var modelFile = 'https://rawcdn.githack.com/mrdoob/three.js/r87/examples/models/mmd/miku/miku_v2.pmd';
			var vmdFiles = [ 'https://rawcdn.githack.com/mrdoob/three.js/r87/examples/models/mmd/vmds/wavefile_v2.vmd' ];

			var splashIndex = 0;
			var splashParticleSize = 0.04;
			var splashMaxNum = 30;
			var splashParticleNum = 400; // for each splash
			var splashVector = new THREE.Vector3( 8, 10, 8 );

			var directionWaveVectors = [
				( new THREE.Vector2( 0.0, 0.7 ) ).normalize(),
				( new THREE.Vector2( -1.0, 1.0 ) ).normalize(),
				( new THREE.Vector2( -0.2, -1.0 ) ).normalize(),
				( new THREE.Vector2( -1.0, -1.0 ) ).normalize(),
				( new THREE.Vector2( 1.0, 0.5 ) ).normalize()
			];
			var directionWaveMaxNum = directionWaveVectors.length;

			var waterHitCheckBoneIndices = [
				82,   // 左髪7
				83,   // 右髪7
				123,  // 左つま先ＩＫ先
				124   // 右つま先ＩＫ先
			];
			var previousBonePositions = [];

			for ( var i = 0, il = waterHitCheckBoneIndices.length; i < il; i ++ ) {

				previousBonePositions.push( new THREE.Vector3() );

			}

			var reflectionCamera;
			var reflectionMatrix = new THREE.Matrix4();
			reflectionMatrix.set(
				1, 0, 0, 0,
				0, -1, 0, 0,
				0, 0, 1, 0,
				0, 0, 0, 1
			);

			var ready = false;

			var textureLoader = new THREE.TextureLoader();

			var clock = new THREE.Clock();

			init();
			animate();

			function init() {

				container = document.createElement( 'div' );
				document.body.appendChild( container );

				// cameras

				camera = new THREE.PerspectiveCamera( 45, window.innerWidth / window.innerHeight, 1, 2000 );
				camera.position.y = 20
				camera.position.z = 50;

				reflectionCamera = camera.clone();

				var controls = new THREE.OrbitControls( camera );

				// scene

				scene = new THREE.Scene();

				// lights

				var ambient = new THREE.AmbientLight( 0x444444 );
				scene.add( ambient );

				var directionalLight = new THREE.DirectionalLight( 0x666666 );
				directionalLight.position.set( -1, 1, 1 ).normalize();
				scene.add( directionalLight );

				// renderer

				renderer = new THREE.WebGLRenderer( { antialias: true } );
				//renderer.setPixelRatio( window.devicePixelRatio );
				renderer.setSize( window.innerWidth, window.innerHeight );
				renderer.setClearColor( new THREE.Color( 0xffffff ) );
				container.appendChild( renderer.domElement );

				effect = new THREE.OutlineEffect( renderer );

				renderTarget = new THREE.WebGLRenderTarget( window.innerWidth, window.innerHeight );

				// model

				helper = new THREE.MMDHelper();

				createModel();
				createWater();
				createSplash();

				//

				window.addEventListener( 'resize', onWindowResize, false );

			}

			function createModel() {

				var onProgress = function ( xhr ) {
					if ( xhr.lengthComputable ) {
						var percentComplete = xhr.loaded / xhr.total * 100;
						console.log( Math.round(percentComplete, 2) + '% downloaded' );
					}
				};

				var onError = function ( xhr ) {
				};

				var loader = new THREE.MMDLoader();

				loader.load( modelFile, vmdFiles, function ( object ) {

					mesh = object;

					mesh.position.y = -0.1;

					helper.add( mesh );
					helper.setAnimation( mesh );
					helper.setPhysics( mesh );

					/*
					 * Note: call this method after you set all animations
					 *       including camera and audio.
					 */
					helper.unifyAnimationDuration( { afterglow: 2.0 } );

					scene.add( mesh );

					ready = true;

				}, onProgress, onError );

			}

			function createWater() {

				var circleWaveTriggerTimesArray = [];
				var circleWaveCentersArray = [];

				for ( var i = 0; i < splashMaxNum; i ++ ) {

					circleWaveTriggerTimesArray.push( 0 );
					circleWaveCentersArray.push( new THREE.Vector2() );

				}

				water = new THREE.Mesh(
					new THREE.PlaneBufferGeometry( waterSize, waterSize, 512, 512 ),
					new THREE.ShaderMaterial( {
						uniforms: {
							size: { value: new THREE.Vector2( window.innerWidth, window.innerHeight ) },
							time: { value: 0 },
							tDiffuse: { value: renderTarget.texture },
							tWaterMap: { value: textureLoader.load( waterTextureFile ) },
							tWaterNormalMap: { value: textureLoader.load( waterNormalTextureFile ) },
							circleWaveCenters: { value: circleWaveCentersArray },
							circleWaveTriggerTimes: { value: circleWaveTriggerTimesArray },
							circleWaveNum: { value: 0 },
							directionWaveVectors: { value: directionWaveVectors },
							directionWaveNum: { value: directionWaveMaxNum }
						},
						vertexShader: document.getElementById( 'vertexShaderWater' ).textContent.replace( '<maxCircleWaveNum>', splashMaxNum ).replace( '<maxDirectionWaveNum>', directionWaveMaxNum ),
						fragmentShader: document.getElementById( 'fragmentShaderWater' ).textContent,
						transparent: true
					} )
				);
				water.rotation.x = -90 * Math.PI / 180;
				scene.add( water );

			}

			function createSplash() {

				var count = splashMaxNum * splashParticleNum;

				var geometry = new THREE.InstancedBufferGeometry();
				geometry.copy( new THREE.SphereBufferGeometry( splashParticleSize ) );

				var translateArray = new Float32Array( count * 3 );
				var vectorArray = new Float32Array( count * 3 );
				var inputTimeArray = new Float32Array( count );

				for ( var i = 0; i < count; i ++ ) {

					translateArray[ i * 3 + 0 ] = 0;
					translateArray[ i * 3 + 1 ] = 0;
					translateArray[ i * 3 + 2 ] = 0;

				}

				for ( var i = 0; i < count; i ++ ) {

					vectorArray[ i * 3 + 0 ] = ( Math.random() - 0.5 ) * splashVector.x;
					vectorArray[ i * 3 + 1 ] = ( Math.random() + 0.5 ) * splashVector.y;
					vectorArray[ i * 3 + 2 ] = ( Math.random() - 0.5 ) * splashVector.z;

				}

				for ( var i = 0; i < count; i ++ ) {

					inputTimeArray[ i ] = 0;

				}

				geometry.addAttribute( 'translate', new THREE.InstancedBufferAttribute( translateArray, 3, 1 ) );
				geometry.addAttribute( 'vector', new THREE.InstancedBufferAttribute( vectorArray, 3, 1 ) );
				geometry.addAttribute( 'inputTime', new THREE.InstancedBufferAttribute( inputTimeArray, 1, 1 ) );

				var material = new THREE.ShaderMaterial( {
					uniforms: {
						time: { value: 0 }
					},
					vertexShader: document.getElementById( 'vertexShaderSplash' ).textContent,
					fragmentShader: document.getElementById( 'fragmentShaderSplash' ).textContent,
					depthTest: true,
					depthWrite: true
				} );

				splash = new THREE.Mesh( geometry, material );

				scene.add( splash );

			}

			function onWindowResize() {

				camera.aspect = window.innerWidth / window.innerHeight;
				camera.updateProjectionMatrix();

				effect.setSize( window.innerWidth, window.innerHeight );
				water.material.uniforms.size.value.set( window.innerWidth, window.innerHeight );
				renderTarget.setSize( window.innerWidth, window.innerHeight );

			}

			function addRipple( x, y ) {

				var uniforms = water.material.uniforms;

				var currentTime = uniforms.time.value;

				uniforms.circleWaveTriggerTimes.value[ splashIndex ] = currentTime;
				uniforms.circleWaveCenters.value[ splashIndex ].set( x / ( waterSize * 0.5 ), -y / ( waterSize * 0.5 ) );
				uniforms.circleWaveNum.value = splashMaxNum;

				for ( var i = 0; i < splashParticleNum; i ++ ) {

					var index = splashIndex * splashParticleNum + i;

					splash.geometry.attributes.inputTime.array[ index + i ] = currentTime;
					splash.geometry.attributes.translate.array[ index * 3 + 0 ] = x;
					splash.geometry.attributes.translate.array[ index * 3 + 1 ] = 0;
					splash.geometry.attributes.translate.array[ index * 3 + 2 ] = y;

				}

				splash.geometry.attributes.inputTime.needsUpdate = true;
				splash.geometry.attributes.translate.needsUpdate = true;

				if ( ++ splashIndex >= splashMaxNum ) splashIndex = 0;

			}

			function getLength( p1, p2 ) {

			     return Math.sqrt( Math.pow( p1.x - p2.x, 2.0 ) + Math.pow( p1.y - p2.y, 2.0 ) + Math.pow( p1.z - p2.z, 2.0 ) );

			}

			function checkWaterHit() {

				var waterHeightThreshold = 0.15;
				var moveThreshold = 0.1;

				var pos = new THREE.Vector3();

				for ( var i = 0, il = waterHitCheckBoneIndices.length; i < il; i ++ ) {

					var boneIndex = waterHitCheckBoneIndices[ i ];
					var previousPosition = previousBonePositions[ i ];

					mesh.skeleton.bones[ boneIndex ].getWorldPosition( pos );

					if ( ( previousPosition.y >= waterHeightThreshold && pos.y < waterHeightThreshold ) ||
					     ( previousPosition.y < waterHeightThreshold && pos.y >= waterHeightThreshold ) ||
					     ( pos.y < waterHeightThreshold && getLength( previousPosition, pos ) > moveThreshold ) ) {

						addRipple( pos.x, pos.z );

					}

					previousPosition.copy( pos );

				}

			}

			function animate() {

				requestAnimationFrame( animate );
				update();
				render();

			}

			function update() {

				if ( ! ready ) return;

				var delta = clock.getDelta();

				helper.animate( delta );

				water.material.uniforms.time.value += delta;
				splash.material.uniforms.time.value += delta;

				checkWaterHit();

			}

			function render() {

				if ( ready ) {

					// pass 1: render reflection to renderTarget for water

					camera.updateMatrixWorld( true );

					reflectionCamera.matrix.copy( reflectionMatrix ).multiply( camera.matrix );
					reflectionCamera.matrix.decompose( reflectionCamera.position, reflectionCamera.quaternion, reflectionCamera.scale );

					mesh.visible = true;
					water.visible = false;
					splash.visible = false;

					renderer.setFaceCulling( THREE.CullFaceFront );

					effect.render( scene, reflectionCamera, renderTarget );

					renderer.setFaceCulling( THREE.CullFaceBack );

					// pass 2: render model

					mesh.visible = true;
					water.visible = false;
					splash.visible = false;

					effect.render( scene, camera );

					// pass 3: render water and splash

					mesh.visible = false;
					water.visible = true;
					splash.visible = true;

					renderer.autoClear = false;

					renderer.render( scene, camera );

					// just in case restore the parameters

					renderer.autoClear = true;

					mesh.visible = true;
					water.visible = true;
					splash.visible = true;

				} else {

					renderer.render( scene, camera );

				}

			}

		</script>

	</body>
</html>
